using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Xml;
using System.IO;
using Ares.PacketHandlers;
using sb0t_winGUI;

namespace cb0tServer
{
    public partial class ServerCore
    {
        private List<UDPFirewallObject> fire_tests = new List<UDPFirewallObject>();
        private List<UDPChatroomObject> rooms = new List<UDPChatroomObject>();
        private Queue<UDPPacket> udp_pending_out = new Queue<UDPPacket>();
        private RoomLocationFinder lf = new RoomLocationFinder();
        private Random udp_rnd = new Random((int)Helpers.UnixTime());

        private int max_rooms_memory = 5000;
        private int max_rooms_disk = 1000;
        private int ackinfo_hits = 0;
        private int sendinfo_hits = 0;
        private int ips_ack_hits = 0;

        private bool received_external_ip = false;

        public enum UDPSortMethodOptions : byte
        {
            SortOldestContactedFirst = 0,
            SortMostRecentFirst = 1,
            SortMostAckedFirst = 2
        };

        public static UDPSortMethodOptions UDPSortMethod = UDPSortMethodOptions.SortOldestContactedFirst;

        private void PushMyRoom()
        {
            if (this.rooms.Count > 1)
            {
                UDPSortMethod = UDPSortMethodOptions.SortOldestContactedFirst;
                this.rooms.Sort();
            }

            if (this.rooms.Count > 0)
            {
                if ((this.rooms[0].last_sent_add_IPS + 900) < Helpers.UnixTime()) // oldest server got our room more than 15 mins ago
                {
                    byte[] packet = this.AddIpsPacket(this.rooms[0]);
                    this.rooms[0].last_sent_add_IPS = (int)Helpers.UnixTime();
                    this.rooms[0].try_count++;
                    this.udp_pending_out.Enqueue(new UDPPacket(this.rooms[0].ToIPEndPoint(), packet, false));
                }
            }
        }

        private void CheckFirewall()
        {
            if (this.udp_firewall_index_check < this.rooms.Count)
            {
                UDPPacket p = new UDPPacket();
                p.ep = this.rooms[this.udp_firewall_index_check].ToIPEndPoint();
                p.packet = this.CheckMyFirewallPacket();
                p.type = false;
                this.udp_pending_out.Enqueue(p);
                this.udp_firewall_index_check++;
            }
        }

        private void ServiceUDPListener()
        {
            // send outgoing first

            if (this.udp_pending_out.Count > 0)
            {
                UDPPacket _packet_out = this.udp_pending_out.Dequeue();

                try
                {
                    this.udp_listener.Send(_packet_out.packet, _packet_out.packet.Length, _packet_out.ep);

                    if (_packet_out.type)
                    {
                        this.ackinfo_hits++;
                        Stats.udp_hits++;
                    }
                }
                catch { }
            }

            // and now receive

            if (this.udp_listener.Available > 0)
            {
                byte[] buffer = new byte[1];
                bool success = false;

                try
                {
                    buffer = this.udp_listener.Receive(ref this.udp_ep);
                    success = true;
                }
                catch { }

                if (success)
                {
                    try
                    {
                        IPEndPoint ep = this.udp_ep;
                        AresDataPacket packet = new AresDataPacket(buffer);
                        UDPChatroomObject rObj;
                        ushort query_host_port;

                        if (packet.Remaining() > 0)
                        {
                            switch (packet.ReadByte())
                            {
                                case 2: // reply with ACKINFO
                                    this.sendinfo_hits++;
                                    buffer = this.AckInfoPacket();
                                    this.udp_pending_out.Enqueue(new UDPPacket(ep, buffer, true));
                                    break;

                                case 11: // reply with ACKIPS (room is pushing onto us)
                                    if (packet.GetByteCount() < 3)
                                        return; // must be at least id + port

                                    query_host_port = packet.ReadInt16();

                                    rObj = this.FindRoom(new UDPChatroomObject(ep.Address, query_host_port));

                                    if (rObj != null) // already got the room
                                    {
                                        rObj.last_received = (int)Helpers.UnixTime();
                                        rObj.port = query_host_port;
                                    }
                                    else
                                    {
                                        rObj = new UDPChatroomObject(ep.Address, query_host_port);
                                        rObj.last_received = (int)Helpers.UnixTime();
                                        this.AddNewServer(rObj);
                                    }

                                    this.PurgeList();

                                    while (packet.Remaining() > 5)
                                    {
                                        rObj = new UDPChatroomObject(packet.ReadIP(), packet.ReadInt16());
                                        this.AddNewServer(rObj);
                                    }

                                    buffer = this.AckIpsPacket(ep.Address);
                                    this.udp_pending_out.Enqueue(new UDPPacket(ep, buffer, false));
                                    break;

                                case 12: // collect ip+ports (server replying to our push)
                                    if (packet.GetByteCount() < 3)
                                        return; // must be at least id + port

                                    this.ips_ack_hits++;
                                    query_host_port = packet.ReadInt16();

                                    rObj = this.FindRoom(new UDPChatroomObject(ep.Address, query_host_port));

                                    if (rObj != null)
                                    {
                                        rObj.port = query_host_port;
                                        rObj.ack_count++;
                                        rObj.last_connect = (int)Helpers.UnixTime();
                                        rObj.last_received = (int)Helpers.UnixTime();
                                        rObj.try_count = 0;
                                    }

                                    while (packet.Remaining() > 5)
                                    {
                                        rObj = new UDPChatroomObject(packet.ReadIP(), packet.ReadInt16());
                                        this.AddNewServer(rObj);
                                    }

                                    break;

                                case 21: // reply with ACKNODES
                                    buffer = UserPool.GetSuperNodes();

                                    if (buffer != null)
                                    {
                                        buffer = this.AckNodesPacket(buffer);
                                        this.udp_pending_out.Enqueue(new UDPPacket(ep, buffer, false));
                                    }

                                    break;

                                case 31: // server wants me to test his firewall
                                    if (packet.Remaining() < 2)
                                        return; // needs port

                                    if (this.fire_tests.Count < 4)
                                    {
                                        uint i_will_test_cookie = (uint)this.udp_rnd.Next(0, 65535);
                                        IPEndPoint firewall_addr = new IPEndPoint(ep.Address, packet.ReadInt16());
                                        this.fire_tests.Add(new UDPFirewallObject(firewall_addr, this.time_now, i_will_test_cookie));
                                        this.udp_pending_out.Enqueue(new UDPPacket(ep, this.ReadyFirewallPacket(firewall_addr.Address, i_will_test_cookie), false));
                                    }
                                    else
                                    {
                                        this.udp_pending_out.Enqueue(new UDPPacket(ep, this.BusyFirewallPacket(ep.Address), false));
                                    }

                                    break;

                                case 32: // server is willing to test my firewall
                                    if (packet.Remaining() < 8)
                                        break; // need cookie + my ip

                                    uint my_test_cookie = packet.ReadInt32();

                                    if (!this.received_external_ip)
                                    {
                                        CoreSettings.room_external_ip = packet.ReadIP();
                                        this.received_external_ip = true;
                                        OtherLogs.WriteCacheLog("Received Server IP Address [" + CoreSettings.room_external_ip.ToString() + "]");
                                        UserPool.RecheckPoolForHosts();
                                        CoreSettings.SaveNew();
                                        this.lf.Query();
                                    }

                                    this.udp_pending_out.Enqueue(new UDPPacket(ep, this.ProceedFirewallPacket(my_test_cookie), false));

                                    if (!this.udp_firewall_is_testing)
                                    {
                                        this.udp_firewall_is_testing = true;
                                        this.udp_firewall_test_start_time = this.time_now;
                                        this.udp_firewall_checkers.Add(ep.Address);
                                        OtherLogs.WriteCacheLog("TCP Firewall Test Begins...");
                                    }

                                    break;

                                case 33: // server wants me to proceed with his firewall test
                                    if (packet.Remaining() < 6)
                                        return; // need port + cookie

                                    query_host_port = packet.ReadInt16();
                                    uint proceed_cookie = packet.ReadInt32();

                                    foreach (UDPFirewallObject fobj in this.fire_tests)
                                        if (fobj.cookie == proceed_cookie)
                                            fobj.StartTest();

                                    break;

                                case 34: // server is too busy to test my firewall, but we collect some servers anyway
                                    while (packet.Remaining() > 5)
                                    {
                                        rObj = new UDPChatroomObject(packet.ReadIP(), packet.ReadInt16());
                                        this.AddNewServer(rObj);
                                    }

                                    break;
                            }
                        }
                    }
                    catch { }

                }
            }
        }

        private void ExpireFirewallTests()
        {
            if (this.fire_tests.Count == 0)
                return;

            for (int i = 0; i < this.fire_tests.Count; i++)
            {
                if ((this.fire_tests[i].spawn + 10) < this.time_now)
                {
                    this.fire_tests[i].Dispose();
                    this.fire_tests.RemoveAt(i);
                    return;
                }
            }
        }

        private void SaveRooms(bool save_regardless_of_acks)
        {
            try
            {
                XmlWriterSettings appearance = new XmlWriterSettings();
                appearance.Indent = true;
                XmlWriter xml = XmlWriter.Create(sb0t_winGUI.RunOnceFileUpdater.sbotpath + "data/udpservers.xml", appearance);

                xml.WriteStartDocument();
                xml.WriteStartElement("nodes");

                UDPSortMethod = UDPSortMethodOptions.SortMostRecentFirst;
                this.rooms.Sort();

                int counter = 0;
                int time = (int)Helpers.UnixTime();

                foreach (UDPChatroomObject c in this.rooms)
                {
                    if (c.ack_count > 0 || save_regardless_of_acks)
                    {
                        if (((time - c.last_connect) < 1800) || save_regardless_of_acks)
                        {
                            if (c.ack_count > 65000)
                                c.ack_count = 40000;

                            xml.WriteStartElement("node");
                            xml.WriteElementString("ip", Convert.ToBase64String(c.ip.GetAddressBytes()));
                            xml.WriteElementString("port", Convert.ToBase64String(BitConverter.GetBytes(c.port)));
                            xml.WriteElementString("ack", Convert.ToBase64String(BitConverter.GetBytes((ushort)c.ack_count)));
                            xml.WriteEndElement();
                            counter++;

                            if (counter > this.max_rooms_disk)
                                break;
                        }
                    }
                }

                xml.WriteEndElement();
                xml.WriteEndDocument();
                xml.Flush();
                xml.Close();

                String udp_stats = "UDP Channel List Updated! [" + this.ips_ack_hits + ":" + this.sendinfo_hits +
                    ":" + this.ackinfo_hits + ":" + this.rooms.Count + ":" + counter + "]";

                OtherLogs.WriteCacheLog(udp_stats);
            }
            catch { }

            this.ips_ack_hits = 0;
            this.sendinfo_hits = 0;
            this.ackinfo_hits = 0;
            Stats.udp_hits = 0;
        }

        private bool LoadRooms()
        {
            this.rooms.Clear();

            try
            {
                FileStream f = new FileStream(sb0t_winGUI.RunOnceFileUpdater.sbotpath + "data/udpservers.xml", FileMode.Open);
                XmlReader xml = XmlReader.Create(new StreamReader(f));

                xml.MoveToContent();
                xml.ReadSubtree().ReadToFollowing("nodes");

                while (xml.ReadToFollowing("node"))
                {
                    UDPChatroomObject c = new UDPChatroomObject();

                    xml.ReadSubtree().ReadToFollowing("ip");
                    c.ip = new IPAddress(Convert.FromBase64String(xml.ReadElementContentAsString()));
                    xml.ReadToFollowing("port");
                    c.port = BitConverter.ToUInt16(Convert.FromBase64String(xml.ReadElementContentAsString()), 0);
                    xml.ReadToFollowing("ack");
                    c.ack_count = (int)BitConverter.ToUInt16(Convert.FromBase64String(xml.ReadElementContentAsString()), 0);

                    this.rooms.Add(c);
                }

                xml.Close();
                f.Flush();
                f.Close();

                return this.rooms.Count > 7;
            }
            catch { }

            return false;
        }

        private void DownloadServerListWorker()
        {
            this.rooms.Clear();

            List<byte> list = new List<byte>(sb0t_winGUI.Properties.Resources.servers); // updated

            while (list.Count >= 6)
            {
                IPAddress ip = new IPAddress(list.GetRange(0, 4).ToArray());
                list.RemoveRange(0, 4);
                ushort port = BitConverter.ToUInt16(list.ToArray(), 0);
                list.RemoveRange(0, 2);
                this.rooms.Add(new UDPChatroomObject(ip, port));
            }

            this.udp_got_servers = true;
            OtherLogs.WriteCacheLog("Spawned default UDP Gcache");
        }

        private void ExpireList()
        {
            if (this.rooms.Count > 1)
            {
                UDPSortMethod = UDPSortMethodOptions.SortMostRecentFirst;
                this.rooms.Sort();
            }

            int i = this.rooms.Count - 1;
            int time = (int)Helpers.UnixTime();

            while (i > 0)
            {
                if (this.rooms[i].try_count > 4)
                {
                    if ((time - this.rooms[i].last_sent_add_IPS) > 60)
                    {
                        if ((time - this.rooms[i].last_connect) > 3600)
                            this.rooms.RemoveAt(i);
                        else
                            break;
                    }
                }

                i--;
            }
        }

        private void PurgeList()
        {
            if (this.rooms.Count > 1)
            {
                UDPSortMethod = UDPSortMethodOptions.SortMostRecentFirst;
                this.rooms.Sort();
            }

            int never_counted = 0;
            int successful = 0;

            foreach (UDPChatroomObject c in this.rooms)
            {
                if (c.last_sent_add_IPS == 0)
                    never_counted++;

                if ((c.last_connect + 900) > Helpers.UnixTime())
                    successful++;
            }

            this.rooms.Reverse();

            if (never_counted <= 600 && successful >= 50)
            {
                for (int i = 0; i < this.rooms.Count; i++)
                {
                    if (this.rooms[i].last_sent_add_IPS == 0)
                        continue;

                    this.rooms.RemoveAt(i);
                    return;
                }
            }

            UDPSortMethod = UDPSortMethodOptions.SortMostAckedFirst;
            this.rooms.Sort();
            this.rooms.RemoveAt(this.rooms.Count - 1);
        }

        private UDPChatroomObject FindRoom(UDPChatroomObject query)
        {
            foreach (UDPChatroomObject c in this.rooms)
                if (c.Equals(query))
                    return c;

            return null;
        }

        private void AddNewServer(UDPChatroomObject obj)
        {
            foreach (UDPChatroomObject c in this.rooms)
                if (c.Equals(obj))
                    return;

            if (Helpers.IsAntiP2P(obj.ip))
                return;

            if (this.rooms.Count > this.max_rooms_memory)
                this.PurgeList();

            this.rooms.Add(obj);
        }

        private void ShuffleList()
        {
            List<UDPChatroomObject> temp = new List<UDPChatroomObject>(this.rooms.ToArray());
            this.rooms.Clear();

            while (temp.Count > 0)
            {
                int i = this.udp_rnd.Next(0, temp.Count);
                this.rooms.Add(temp[i]);
                temp.RemoveAt(i);
            }
        }

        private byte[] AckInfoPacket()
        {
            AresDataPacket packet = new AresDataPacket();
            packet.WriteByte(3);
            packet.WriteInt16(CoreSettings.room_port); // my room port
            packet.WriteInt16(UserPool.UserCount()); // my user count
            packet.WriteInt16((ushort)Encoding.UTF8.GetByteCount(CoreSettings.GetUDPRoomName()));
            packet.WriteString(CoreSettings.GetUDPRoomName(), false);
            packet.WriteInt16((ushort)Encoding.UTF8.GetByteCount(Topics.GetUDPTopic()));
            packet.WriteString(Topics.GetUDPTopic(), false);
            packet.WriteByte(Helpers.LangCIndexToCode(CoreSettings.room_language)); // language
            packet.WriteInt16(15);
            packet.WriteString("sb0t chatserver", false);
            packet.WriteByte(0);

            int _index = packet.GetByteCount() - 1;

            if (this.rooms.Count > 1)
                this.ShuffleList();

            int counter = 0;

            foreach (UDPChatroomObject c in this.rooms)
            {
                if (c.ack_count > 0)
                {
                    if ((c.last_connect + 900) > Helpers.UnixTime())
                    {
                        packet.WriteIP(c.ip);
                        packet.WriteInt16(c.port);
                        counter++;

                        if (counter == 7)
                            break;
                    }
                }
            }

            if (counter > 0)
                packet.ReplaceByte((byte)counter, _index);

            return packet.ToByteArray();
        }

        private byte[] CheckMyFirewallPacket()
        {
            AresDataPacket packet = new AresDataPacket();
            packet.WriteByte(31);
            packet.WriteInt16(CoreSettings.room_port);
            return packet.ToByteArray();
        }

        private byte[] ReadyFirewallPacket(IPAddress target_external_ip, uint i_will_test_cookie)
        {
            AresDataPacket packet = new AresDataPacket();
            packet.WriteByte(32);
            packet.WriteInt32(i_will_test_cookie);
            packet.WriteIP(target_external_ip);
            return packet.ToByteArray();
        }

        private byte[] ProceedFirewallPacket(uint cookie)
        {
            AresDataPacket packet = new AresDataPacket();
            packet.WriteByte(33);
            packet.WriteInt16(CoreSettings.room_port);
            packet.WriteInt32(cookie);
            return packet.ToByteArray();
        }

        private byte[] BusyFirewallPacket(IPAddress org)
        {
            AresDataPacket packet = new AresDataPacket();
            packet.WriteByte(34);

            if (this.rooms.Count > 1)
                this.ShuffleList();

            int counter = 0;

            foreach (UDPChatroomObject c in this.rooms)
            {
                if (!org.Equals(c.ip))
                {
                    if (c.ack_count > 0)
                    {
                        if ((c.last_connect + 900) > Helpers.UnixTime())
                        {
                            packet.WriteIP(c.ip);
                            packet.WriteInt16(c.port);
                            counter++;

                            if (counter == 6)
                                break;
                        }
                    }
                }
            }

            return packet.ToByteArray();
        }

        private byte[] AddIpsPacket(UDPChatroomObject org)
        {
            AresDataPacket packet = new AresDataPacket();
            packet.WriteByte(11);
            packet.WriteInt16(CoreSettings.room_port); // my room port

            if (this.rooms.Count > 1)
                this.ShuffleList();

            int counter = 0;

            foreach (UDPChatroomObject c in this.rooms)
            {
                if (!org.Equals(c))
                {
                    if (c.ack_count > 0)
                    {
                        if ((c.last_connect + 900) > Helpers.UnixTime())
                        {
                            packet.WriteIP(c.ip);
                            packet.WriteInt16(c.port);
                            counter++;

                            if (counter == 6)
                                break;
                        }
                    }
                }
            }

            return packet.ToByteArray();
        }

        private byte[] AckIpsPacket(IPAddress ip)
        {
            AresDataPacket packet = new AresDataPacket();
            packet.WriteByte(12);
            packet.WriteInt16(CoreSettings.room_port); // my room port

            if (this.rooms.Count > 1)
                this.ShuffleList();

            int counter = 0;

            foreach (UDPChatroomObject c in this.rooms)
            {
                if (!ip.Equals(c.ip))
                {
                    if (c.ack_count > 0)
                    {
                        if ((c.last_connect + 900) > Helpers.UnixTime())
                        {
                            packet.WriteIP(c.ip);
                            packet.WriteInt16(c.port);
                            counter++;

                            if (counter == 6)
                                break;
                        }
                    }
                }
            }

            return packet.ToByteArray();
        }

        private byte[] SendNodesPacket()
        {
            return new byte[] { 21 };
        }

        private byte[] AckNodesPacket(byte[] node_data)
        {
            AresDataPacket packet = new AresDataPacket();
            packet.WriteByte(22);
            packet.WriteInt16(CoreSettings.room_port); // my room port
            packet.WriteBytes(node_data);
            return packet.ToByteArray();
        }

    }
}
